﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using PaintDotNet;
using PaintDotNet.Effects;

namespace LibFiltr
{
    public class BilateralFilter
    {
        public BilateralFilter(int radius, double sigma_d = 1.0, double sigma_r = 1.0)
        {
            _sigma_d = sigma_d;
            _sigma_r = sigma_r;
            _radius = radius;
            _kernel = new double[Size, Size];
            _kernel_ePow = new double[Size, Size];

            double twoSigma_d_Square = 2 * Math.Pow(_sigma_d, 2);
            
            for (int i = -_radius; i <= _radius; i++)
                for (int j = -_radius; j <= _radius; j++)
                {
                    double ePow = -(Math.Pow(j, 2) + Math.Pow(i, 2)) / twoSigma_d_Square;
                    _kernel_ePow[i + _radius, j + _radius] = ePow;
                }
        }

        public int Size
        {
            get { return 2 * _radius + 1; }
        }

        //public Surface SurfaceSource
        //{
        //    get { return _surface_src; }
        //    set { _surface_src = value; }
        //}

        public void Filter(Surface dst, Surface src, Rectangle selection, Rectangle rect)
        {
            ColorBgra CurrentPixel;

            for (int y = rect.Top; y < rect.Bottom; y++)
            {
                for (int x = rect.Left; x < rect.Right; x++)
                {
                    CurrentPixel = src[x, y];

                    CurrentPixel.R = (byte)CalcChanel(ref selection, ref src, x, y, Chanel.Red);
                    CurrentPixel.G = (byte)CalcChanel(ref selection, ref src, x, y, Chanel.Green);
                    CurrentPixel.B = (byte)CalcChanel(ref selection, ref src, x, y, Chanel.Blue);

                    dst[x, y] = CurrentPixel;
                }
            }
        }

        private enum Chanel
        {
            Red,
            Green,
            Blue

        }

        private double CalcChanel(ref Rectangle selection, ref Surface src, int x, int y, Chanel chanel)
        {
            double sum = 0.0;

            CorrectKernel(x, y, ref selection, ref src, chanel);

            for (int i = -_radius; i <= _radius; i++)
                for (int j = -_radius; j <= _radius; j++)
                    sum += _kernel[i + _radius, j + _radius];

            return sum;
        }

        private void CorrectKernel(int x, int y, ref Rectangle selection, ref Surface src, Chanel chanel)
        {
            double twoSigma_r_Square = 2 * Math.Pow(_sigma_r, 2);
            double kernelSum = 0.0;
            int centraly = Math.Max(y + _radius, selection.Top), centralx = Math.Max(x + _radius, selection.Left);
            centraly = Math.Min(centraly, selection.Bottom - 1); centralx = Math.Min(centralx, selection.Right - 1);

            for (int i = -_radius; i <= _radius; i++)
                for (int j = -_radius; j <= _radius; j++)
                {
                    // check for margins
                    int ipos = Math.Max(y + i, selection.Top), jpos = Math.Max(x + j, selection.Left);
                    ipos = Math.Min(ipos, selection.Bottom - 1); jpos = Math.Min(jpos, selection.Right - 1);

                    double correct_ePow = .0;
                    switch (chanel)
                    {
                        case Chanel.Red:
                            correct_ePow = -Math.Pow((src[jpos, ipos].R - src[centralx, centraly].R), 2) / twoSigma_r_Square;
                            break;
                        case Chanel.Green:
                            correct_ePow = -Math.Pow((src[jpos, ipos].G - src[centralx, centraly].G), 2) / twoSigma_r_Square;
                            break;
                        case Chanel.Blue:
                            correct_ePow = -Math.Pow((src[jpos, ipos].B - src[centralx, centraly].B), 2) / twoSigma_r_Square;
                            break;
                    }

                    double total_ePow = _kernel_ePow[i + _radius, j + _radius] + correct_ePow;

                    _kernel[i + _radius, j + _radius] = Math.Exp(total_ePow);
                    kernelSum += _kernel[i + _radius, j + _radius];

                    switch (chanel)
                    {
                        case Chanel.Red:
                            _kernel[i + _radius, j + _radius] *= src[jpos, ipos].R;
                            break;
                        case Chanel.Green:
                            _kernel[i + _radius, j + _radius] *= src[jpos, ipos].G;
                            break;
                        case Chanel.Blue:
                            _kernel[i + _radius, j + _radius] *= src[jpos, ipos].B;
                            break;
                        default:
                            break;
                    }
                }

            for (int i = -_radius; i <= _radius; i++)
                for (int j = -_radius; j <= _radius; j++)
                    _kernel[i + _radius, j + _radius] /= kernelSum;

        }

        private double _sigma_d;
        private double _sigma_r;
        private int _radius;
        private double[,] _kernel;
        private double[,] _kernel_ePow;
        //private Surface _surface_src;
    }
}
